DockerでRailsの開発環境を構築する
モバイルアプリサービス部の五十嵐です。
先日、あるRailsアプリケーションの開発環境を同僚のマシンに作成しようとしたところ、gemのインストールに1日かかってしまいました。環境構築は手順化されていたのですが、トラブったのは主にNative Moduleを利用する libv8
、 therubyracer
、 rmagick
などのおなじみの面々です。手順を作った時は、これらのgemのインストールに必要なライブラリを brew install
で最新バージョンをインストールするだけでよかったのですが、時が経ちライブラリの最新バージョンが更新されていたことが主な原因でした。この状況はいかんな〜と思い、Railsアプリケーションの開発環境もDockerにすることにしました。
本記事では、Railsアプリケーションの開発環境をDockerにするときに検討したことや問題点などを書いています。なお、本記事で紹介するDockerfileやdocker-compose.ymlは開発環境用なので、本番環境の参考にはしないでください。
アジェンダ
- セットアップ
- デバッグ
- Tips
- 成果物
セットアップ
Railsアプリケーションのセットアップには、主に bundle install
と rake
コマンドの実行があります。これらそれぞれをタイミングで行うのが良いか検討しましました。
bundle install
結論としてはDockerfileに含めてbuildの段階で実行することにしました。理由は、Gemfileの変更がそれほど頻繁ではないこと、Gemfileを変更しなければbuild時にキャッシュが効くので時間がかからないことなどがあります。また、コンテナ起動後にGemfileを変更した場合は、 docker-compose exec rails /bin/bash
でコンテナのシェルを起動し、 bundle install
することでgemの更新ができます。
もう一つのアイディアとして、Data Volume Container を作成し、そこにgemをインストールする方法を試しました。このメリットは、volumeを削除しない限りインストールしたgemのデータを再利用できるため、Gemfileを頻繁に更新してもbuildが素早くできるということです。ただし起動時にはgemがインストールされている必要があるので、どこかのタイミングで docker-compose run --rm rails bundle install
などを実行する必要があります。起動時にcommandなどで bundle install
するのは、起動が遅くなるのがいまいちでした。しかしこの方法には、DockerのVolumeの読み込みが非常に遅いという大きな問題があったので諦めました。この問題についてはDocker for Macが遅い問題をdocker-syncで解決する - Cluex Developersブログが詳しいです。
採用はしませんでしたが、Data Volume Container を使う設定はこんなかんじです。不要な箇所は省いています。docker-compose.yml の version:3 から volumes_from
がなくなっていて、代わりの書き方を調べるのに苦労しました。いまだにしっくりきません。もしかして間違っているかも。
version: "3" services: rails: build: . command: bundle exec rails s -p 3000 -b '0.0.0.0' volumes: - .:/myapp - bundle-volume:/myapp/vendor/bundle ports: - 3000:3000 datastore: build: context: . dockerfile: Dockerfile-datastore volumes: - bundle-volume:/myapp/vendor/bundle volumes: bundle-volume:
datastoreのイメージを作成するための Dockerfile。
FROM busybox:latest RUN mkdir -p /myapp/vendor/bundle VOLUME /myapp/vendor/bundle
Data Volume Container は複数のコンテナ間でvolumeを共有するための方法です。私もまだ理解が怪しいので、詳しくはManage data in containers - Docker Documentationを参照してください。
rake コマンド
結論としては手動で docker-compose run --rm rails rake db:migrate
などを実行することにしました。初期化用にスクリプトにまとめておいてもいいかもしれません。前述した通り、起動時のcommandに設定してしまうと、必要もないのにコマンドが実行され毎回の起動が遅くなるため、起動とは分けました。
デバッグ
次にデバッグの方法について説明します。
byebug, pry-byebug
コンテナ内で起動しているRailsアプリケーションに対しての、byebug
や pry-byebug
を利用したデバッグは、コンテナに Attach することでできます。Attachしてコンテナの外から命令を送るために stdin_open: true
と tty: true
の2つの設定が必要になります。
version: "3" services: rails: build: . command: /myapp/script/start.sh volumes: - .:/myapp ports: - 1234:1234 # debug port - 3000:3000 stdin_open: true tty: true
これで docker-compose up -d
を実行し、アプリケーションが起動した後に docker attach xxx_rails_1
を実行します。すると以下のようにdebug画面が表示されます。以下は byebug の例です。
[8, 17] in /myapp/app/controllers/messages_controller.rb 8: @per = params[:per] || 20 9: @page = params[:page] || 1 10: @message = Search::Message.new(search_params) 11: @messages = @message.matches.page(@page).per(@per) 12: byebug => 13: end 14: 15: # Message新規作成画面を表示 16: def new 17: @message = Message.new (byebug) @per 20 (byebug)
Attach したコンテナから抜ける場合は、 Control + P, Q
でコンテナを終了せずコンソールだけ抜けます。 Control + C
をするとコンテナが終了してしまいます。
参考: Docker and ruby on rails – Dominik Burgdörfer – Medium
Intellij IDEA, RubyMine
Intellij IDEA や RubyMine でもデバッグ機能を使うことができます。まずはじめに「intellij docker rails debug」とかで検索するとRubyMine 2017.1 Help :: Docker - Creating a Docker Deployment run/debug configurationという、いかにもデバッグ実行ができそうなドキュメントが見つかるのですが、こちらはIntellijからDockerやDockerCopmoseを起動する設定で、デバッグではありませんでした。
よくよく調べると、Intellij IDEA や RubyMine のRubyのデバッグ機能は「Ruby remote debug」というIDEの機能と ruby-debug-ide
という gem を使っているということが分かり、こちらの記事に UPDATED: Running the Rails debugger in a Docker container using RubyMine on a Mac にやり方がそのまま載っていました。
まず IDE の RUN > Edit Configuration... を開き、「+」ボタンから「Ruby remote debug」を追加します。設定は以下のとおりです。
Gemfileに以下を追加します。
group :development, :test do # remote debug gem 'ruby-debug-ide' gem 'debase' end
rdebug-ide で Railsサーバを起動します。
bundle exec rdebug-ide --host 0.0.0.0 --port 1234 --dispatcher-port 26162 -- /myapp/bin/rails s -b 0.0.0.0 -p 3000 -e development
以下のメッセージが表示されますので、この状態で先ほど作成した「Ruby remote debug」を実行します。
Fast Debugger (ruby-debug-ide 0.6.0, debase 0.2.1, file filtering is supported) listens on 0.0.0.0:1234
Rails serverが起動します。
Fast Debugger (ruby-debug-ide 0.6.0, debase 0.2.1, file filtering is supported) listens on 0.0.0.0:1234 => Booting WEBrick => Rails 4.2.4 application starting in development on http://0.0.0.0:3000 => Run `rails server -h` for more startup options => Ctrl-C to shutdown server
あとは、Intellijにブレイクポイントを設定して通常と同じデバッグができます。ただ、ステップインでソースコードが移動しないなど、完璧な動作ではありませんでした。
Ruby remote debug の トラブルシューティング
ブレイクポイントで止まらない
Cannot render console from 172.19.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
このエラーが出力されている場合、config/environments/development.rb
に以下を追加します。
config.web_console.whitelisted_ips = '0.0.0.0/0'
ブレイクポイントでNoMethodErrorが発生する
`undefined method '+' for nil:NilClass` Trace log byebug (5.0.0) lib/byebug/processors/command_processor.rb:27:in `at_breakpoint' byebug (5.0.0) lib/byebug/context.rb:78:in `at_breakpoint' app/controllers/messages_controller.rb:16:in `new'
byebug
のバージョンが古いことが問題でした。 byebug
のバージョンを最新にします。
Tips
プロセスの起動ファイルが消えない場合がある
docker stop
したときに、 tmp/pids/server.pid
が削除されず、次に docker start
した時にRailsアプリケーションが起動できないことがあります。雑な対応ですが、 docker start
時に実行するシェルに rm /myapp/tmp/pids/server.pid
を1行追加して対処しました。
CircleCI 2.0 のホスト名の解決
docker-compose で depends_on したサービスのホスト名はサービス名でDNS解決するのですが、CircleCI 2.0 では 127.0.0.1 で解決するので設定に差異が生まれました。これはホスト名を環境変数にすることで対処しました。また、ホスト名のデフォルト値を 127.0.0.1 にすることで、Dockerを使わないローカル環境の起動もできるようにしています。
version: 2 jobs: build: working_directory: ~/my-app docker: - image: ruby:2.2.3 - image: mysql:5.7.11 environment: MYSQL_ROOT_PASSWORD: password TZ: JST - image: fingershock/dynamodb-local 以下略
test: host: <%= ENV['RDS_HOSTNAME'] || '127.0.0.1' %>
成果物
最後に完成した Dockerfile と docker-compose.yml を紹介をします。Docker公式のドキュメントにRailsアプリケーション用のDockerfileとDockerComposeのサンプルがあるのでこれをベースとしました。
Quickstart: Compose and Rails - Docker Documentation
FROM ruby:2.2.3 RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs # aws-cliのインストール RUN apt-get install -y python2.7-dev \ && curl -O https://bootstrap.pypa.io/get-pip.py \ && python get-pip.py \ && pip install awscli RUN mkdir /myapp WORKDIR /myapp ENV BUNDLE_JOBS=4 ADD Gemfile /myapp/Gemfile ADD Gemfile.lock /myapp/Gemfile.lock RUN bundle install
Railsアプリケーションのセットアップに aws-cli
が必要なので、追加してインストールしています。
version: "3" services: mysql: image: mysql:5.7.11 ports: - 3306:3306 environment: - MYSQL_ROOT_PASSWORD=password dynamodb: image: fingershock/dynamodb-local ports: - 8000:8000 rails: build: . command: /myapp/script/start.sh environment: AWS_DEFAULT_REGION: ap-northeast-1 AWS_ACCESS_KEY_ID: dummy AWS_SECRET_ACCESS_KEY: dummy DYNAMODB_ENDPOINT: http://dynamodb:8000 RDS_HOSTNAME: mysql volumes: - .:/myapp ports: - 1234:1234 # debug port - 3000:3000 depends_on: - mysql - dynamodb stdin_open: true tty: true
#!/bin/bash # プロセスが正常終了しなかったときに、 server.pid ファイルが残ることがあるので削除する。 rm /myapp/tmp/pids/server.pid bundle exec rails s -p 3000 -b '0.0.0.0' -e development
#!/bin/bash # プロセスが正常終了しなかったときに、 server.pid ファイルが残ることがあるので削除する。 rm /myapp/tmp/pids/server.pid bundle exec rdebug-ide --host 0.0.0.0 --port 1234 --dispatcher-port 26162 -- /myapp/bin/rails s -b 0.0.0.0 -p 3000 -e development
さいごに
これでローカル環境にRubyすらなくてもRailsアプリケーションの開発ができるようになりました。開発環境の構築も10分くらいで済むでしょう。トレードオフとして開発者はある程度のDockerの知識が必要となり、慣れるまで1日くらいかかるかもしれません :)